use std::{
    fs::File,
    io::{Read, Write},
    path::PathBuf,
    thread,
    time::{Duration, Instant},
};

use anyhow::{anyhow, Result};
use clap::{Parser, Subcommand};
use ini::Ini;
use serde::{Deserialize, Serialize};

const SOURCE_PATH: &str = "proxyup/proxy_source.ini";
const CONFIG_PATH: &str = "proxyup/config.toml";
const STUNNEL_CONF: &str = "proxyup/stunnel.conf";

/// proxyup - set command line proxy
#[derive(Debug, Parser)]
#[command(version)]
struct Cli {
    /// proxy.ini file path
    #[arg(short, long, value_name = "FILE")]
    path: Option<PathBuf>,
    #[command(subcommand)]
    command: Commands,
}

#[derive(Debug, Subcommand)]
enum Commands {
    /// show proxys to use
    List,
    /// set proxy to use
    Set {
        name: Option<String>,
        /// not use stunnel
        #[arg(short, long)]
        no_stunnel: bool,
    },
    /// unset proxy
    Unset,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
struct Proxy {
    name: String,
    schema: String,
    url: String,
    port: String,
    user: String,
    pass: String,
}

impl Proxy {
    fn from_ini(k: &str, v: &str) -> Self {
        let mut iter = v.split(',');
        let schema = iter.next().unwrap_or_default().to_string();
        let url = iter.next().unwrap_or_default().to_string();
        let port = iter.next().unwrap_or_default().to_string();
        let user = iter.next().unwrap_or_default().to_string();
        let pass = iter.next().unwrap_or_default().to_string();
        Self {
            name: k.to_owned(),
            schema,
            url,
            port,
            user,
            pass,
        }
    }

    fn get_url(&self) -> String {
        format!("{}://{}:{}", self.schema, self.url, self.port)
    }

    fn get_url_with_user(&self) -> String {
        format!(
            "{}://{}:{}@{}:{}",
            self.schema, self.user, self.pass, self.url, self.port
        )
    }

    fn get_url_stunnel(&self) -> String {
        format!("http://{}:{}@127.0.0.1:10081", self.user, self.pass)
    }
}

fn main() -> Result<()> {
    let cli = Cli::parse();

    match cli.command {
        Commands::List => {
            list_proxys(cli.path)?;
            Ok(())
        }
        Commands::Set { name, no_stunnel } => set_proxy(cli.path, name, no_stunnel),
        Commands::Unset => unset_proxy(),
    }
}

fn list_proxys(path: Option<PathBuf>) -> Result<Option<Proxy>> {
    let proxys = get_proxys(path)?;

    let mut handles = vec![];
    for proxy in proxys {
        handles.push(thread::spawn(move || calc_delay(proxy)));
    }

    println!("testing proxy delay...");

    let mut list = vec![];
    for handle in handles {
        let (proxy, duration) = handle.join().unwrap();
        if duration > 0 {
            list.push((proxy, duration));
        }
    }
    list.sort_by(|a, b| a.1.cmp(&b.1));
    list = list.into_iter().take(10).collect();
    for (proxy, duration) in &list {
        println!("{} - {}ms", proxy.name, duration);
    }

    Ok(list.first().map(|(p, _)| p).cloned())
}

fn get_proxys(path: Option<PathBuf>) -> Result<Vec<Proxy>> {
    let path = path.unwrap_or_else(|| {
        if let Some(dir) = dirs::config_dir() {
            dir.join(SOURCE_PATH)
        } else {
            PathBuf::from(SOURCE_PATH)
        }
    });
    let config = Ini::load_from_file(path)?;

    let proxys = config
        .iter()
        .find_map(|(sec, prop)| {
            if sec == Some("Proxy") {
                let proxys: Vec<Proxy> = prop.iter().map(|(k, v)| Proxy::from_ini(k, v)).collect();
                Some(proxys)
            } else {
                None
            }
        })
        .ok_or(anyhow!("no usable proxys"))?;

    Ok(proxys)
}

fn calc_delay(proxy: Proxy) -> (Proxy, u128) {
    let mut handles = vec![];
    for _ in 0..5 {
        let p = proxy.clone();
        handles.push(thread::spawn(move || send_request(p)));
    }
    let mut sum = 0;
    for handle in handles {
        let dur = match handle.join().unwrap() {
            Ok(d) => d.as_millis(),
            Err(_) => 0,
        };
        if dur > 0 {
            sum = (sum + dur) / 2;
        }
    }
    (proxy, sum)
}

fn send_request(proxy: Proxy) -> Result<Duration> {
    let now = Instant::now();
    let proxy = reqwest::Proxy::all(proxy.get_url())?.basic_auth(&proxy.user, &proxy.pass);
    let client = reqwest::blocking::Client::builder()
        .proxy(proxy)
        .timeout(Duration::from_secs(5))
        .build()?;
    let _ = client.get("https://www.google.com").send()?;
    Ok(now.elapsed())
}

fn set_proxy(path: Option<PathBuf>, name: Option<String>, no_stunnel: bool) -> Result<()> {
    let mut proxy = read_proxy_from_config().unwrap_or_default();

    if let Some(name) = name {
        if proxy.name != name {
            let proxys = get_proxys(path)?;
            proxy = proxys
                .into_iter()
                .find(|p| p.name == name)
                .ok_or(anyhow!("not found this {} proxy", name))?;
            if let Err(e) = save_proxy_to_config(&proxy) {
                println!("save proxy config fail, error: {}", e)
            }
        }
    }

    if proxy.name.is_empty() {
        proxy = list_proxys(None)?.ok_or(anyhow!("no proxy can use"))?;
        if let Err(e) = save_proxy_to_config(&proxy) {
            println!("save proxy config fail, error: {}", e)
        }
    }

    export_proxy(proxy, no_stunnel);

    Ok(())
}

fn read_proxy_from_config() -> Result<Proxy> {
    let path = if let Some(dir) = dirs::config_dir() {
        dir.join(CONFIG_PATH)
    } else {
        PathBuf::from(CONFIG_PATH)
    };
    let mut f = File::open(path)?;
    let mut content = String::new();
    f.read_to_string(&mut content)?;
    let proxy: Proxy = toml::from_str(&content)?;
    Ok(proxy)
}

fn save_proxy_to_config(proxy: &Proxy) -> Result<()> {
    let content = toml::to_string(proxy)?;
    write_file(CONFIG_PATH, content)?;

    let content = format!(
        "[vpn-proxy]\nclient = yes\naccept = 127.0.0.1:10081\nconnect = {}:{}",
        proxy.url, proxy.port
    );
    write_file(STUNNEL_CONF, content)?;

    Ok(())
}

fn write_file(path: &str, content: String) -> Result<()> {
    let path = if let Some(dir) = dirs::config_dir() {
        dir.join(path)
    } else {
        PathBuf::from(path)
    };
    let mut f = File::create(path)?;
    f.write_all(content.as_bytes())?;
    f.flush()?;
    Ok(())
}

fn export_proxy(proxy: Proxy, no_stunnel: bool) {
    let url = if no_stunnel {
        proxy.get_url_with_user()
    } else {
        proxy.get_url_stunnel()
    };
    println!(
        "export http_proxy=\"{}\"\nexport https_proxy=\"{}\"",
        url, url
    );
}

fn unset_proxy() -> Result<()> {
    println!("unset http_proxy\nunset https_proxy");
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_from_ini() {
        let p = Proxy::from_ini(
            "G1.Esg2",
            "https,g1-Esg2.go2https.com,20002,galuser,affa9791b",
        );
        println!("{:?}", p);
    }

    #[test]
    fn test_get_proxys() {
        let proxys = get_proxys(None).unwrap();
        println!("proxys: {:?}", proxys);
    }

    #[test]
    fn test_send_request() {
        let proxy = Proxy {
            name: "G1.s1".into(),
            schema: "https".into(),
            url: "g1-s1.go2https.com".into(),
            port: "20023".into(),
            user: "galuser".into(),
            pass: "affa9791b".into(),
        };
        match send_request(proxy) {
            Ok(d) => println!("duration: {:?}", d),
            Err(e) => println!("{:?}", e),
        }
        let proxy = Proxy {
            name: "G1.jp2".into(),
            schema: "https".into(),
            url: "g1-jp2.go2https.com".into(),
            port: "20019".into(),
            user: "galuser".into(),
            pass: "affa9791b".into(),
        };
        match send_request(proxy) {
            Ok(d) => println!("duration: {:?}", d),
            Err(e) => println!("{:?}", e),
        }
    }

    #[test]
    fn test_calc_delay() {
        let proxy = Proxy {
            name: "G1.jp2".into(),
            schema: "https".into(),
            url: "g1-jp2.go2https.com".into(),
            port: "20019".into(),
            user: "galuser".into(),
            pass: "affa9791b".into(),
        };
        calc_delay(proxy);
        let proxy = Proxy {
            name: "G1.s1".into(),
            schema: "https".into(),
            url: "g1-s1.go2https.com".into(),
            port: "20023".into(),
            user: "galuser".into(),
            pass: "affa9791b".into(),
        };
        calc_delay(proxy);
    }

    #[test]
    fn test_list_proxys() {
        match list_proxys(None) {
            Ok(Some(proxy)) => println!("proxy: {:?}", proxy),
            Ok(None) => println!("no proxy"),
            Err(e) => println!("{:?}", e),
        }
    }

    #[test]
    fn test_read_proxy_from_config() {
        match read_proxy_from_config() {
            Ok(proxy) => println!("proxy: {:?}", proxy),
            Err(e) => println!("{:?}", e),
        }
    }

    #[test]
    fn test_save_proxy_to_config() {
        let proxy = Proxy {
            name: "G1.s1".into(),
            schema: "https".into(),
            url: "g1-s1.go2https.com".into(),
            port: "20023".into(),
            user: "galuser".into(),
            pass: "affa9791b".into(),
        };
        if let Err(e) = save_proxy_to_config(&proxy) {
            println!("{:?}", e);
        }
    }
}
